【祝1.0🥟】Bunランタイム上で動作するHTTP APIをApp Runnerへホスティングしてみた
はじめに
9月8日(現地時間)にJavaScriptランタイムのBunのバージョン1.0がリリースされました🥳🥟 本記事では、Bunランタイムを生かしたHTTP APIを作ってApp Runnerにホスティングしてみます。
Bunとは
JavaScriptランタイムは現在 Node.js
Deno
Bun
と複数ある状況で、それぞれ異なる要素技術で作られています。詳しくはBun first impressions techfeedを参考にするのが分かりやすいです。よく挙げられる点として、ネイティブ実装にZigの採用、JITコンパイラとしてJavaScriptCoreを採用している点です。1.0の紹介動画であるBun 1.0 is hereでは速度の話がよく出てきており、強く押し出している印象を受けました。
設計ゴールに関しては、bun.sh | Design Goalsが参考になります。
構成
構成は以下の通りです。Bunのバージョンは1.0.1
を利用します。この肉まんのアイコンかわいいですね。
本記事の主旨としてなるべくBunを活かす構成を考えました。結果、Bunの組み込み関数で特徴的なbun:sqlite
を活用してTODOのHTTP APIを作成してみます。構成の詳細は以下の通りです。
要素 | 採用 | 理由 |
---|---|---|
ホスティング先 | App Runner | Lambdaのカスタムランタイムがありますが、ネイティブサポートされていないため現時点では現実採用されにくいため |
データベース | SQLite3 | BunにはSQLite3ドライバがネイティブ(bun:sqlite )で実装されているため |
冗長化構成 | Litestream | SQLite3とS3をレプリケートするため |
HTTP Webフレームワーク | Hono | Bunをサポートしているため |
ソースコード
ソースコードはGitHubにあります。bun
コマンドで全て完結します。(コードフォーマットだけ手軽に行きたいのでdeno fmt
を使っています。。)
github.com/shuntaka9576/bun-apprunner-template
Quick Start
以下のコマンドで、環境構築ホスティング全て完了します。本プロジェクトでは、Dockerコンテナクライアントとしてfinch
の利用を推奨します。
cd ./packages/aws export CDK_DOCKER=$(which finch) bunx cdk deploy bun-apprunner-app
処理としては、以下の通りです。CDKが全部やってくれます。
- ローカルでコンテナビルド
- コンテナをECRへpush
- App RunnerやS3のリソースを作成
- コンテナのデプロイ
ローカルでコンテナをビルドします。ローカルのDocker環境が整備されていることを確認してください。App Runnerは、x86である必要があるためarmだと動作しません。今回コンテナではバイナリ含め全てx86でビルドされたものを利用しているので互換性はありません。
CDK実行結果
✨ Synthesis time: 1.65s This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: (中略) IAM Statement Changes bun-apprunner-app.BunApprunnerHostingConstructAppRunnerUriAEE3023C = https://xxxxxxxx.ap-northeast-1.awsapprunner.com <- このURLを利用
API利用例
# GET /tasks $ curl -v https://xxxxxxxx.ap-northeast-1.awsapprunner.com/tasks (中略) {"tasks":[]} # POST /tasks $ curl -v -X POST https://xxxxxxxx.ap-northeast-1.awsapprunner.com/tasks -d '{"title": "foo"}' (中略) {"taskId":"71f21d95-b4f6-4327-9134-452ed346184a","title":"foo","createAt":"2023/09/13 17:09:13"} # GET /tasks/:taskId $ curl -v https://xxxxxxxx.ap-northeast-1.awsapprunner.com/tasks/71f21d95-b4f6-4327-9134-452ed346184a (中略) {"taskId":"71f21d95-b4f6-4327-9134-452ed346184a","title":"foo","createAt":"2023/09/13 17:09:13"}
解説
一部しか紹介しないため、気になる点がありましたら実際のソースコードを参照してください。
bunコマンド
bun
コマンドは、npm
yarn
pnpm
に当たるコマンドです。特に後述する依存のインストールはかなり高速です。またworkspace
に対応しています。ただ、-w
オプションは現時点ではないようなので、特定のパスに行ってパッケージ追加が必要です。よく使うコマンドを紹介します。
依存のインストール
--froze-lockfile
や--production
オプションもありますが、コンテナ内部だと矛盾が出てしまいうまく動作しませんでした。故に後述のDockerfileではオプションを指定していません。
bun install
スクリプト実行
--hot
でホットリロードが可能です。HTTP API実装では重宝します。--bun
オプションは、bunランタイムに動作を強制させます。既存のNext.jsやNestJSアプリが起動するのは、このオプションを設定していないためで、裏側でnodeプロセスが起動しています。
bun run --hot --bun ./src/main.ts
bunx
はnpx
のようなコマンド、cdk
やeslint
などのCLIを利用する際に利用します。
bunx cdk deploy bun-apprunner-app
コンテナイメージ
マルチステージビルドで、litestream
とbun
を取得して、アプリ側のコンテナへ移動しています。イメージサイズは約90MBでした。
bun
は*-baseline
のバイナリを利用しています。baselineなしのバイナリだと、コンテナでbunコマンド起動時にクラッシュすることがあったためです。"Illegal instruction (core dumped)" when using bun through code-serverが起因していそうです。MacOSでビルドしたので、Linuxだとまた結果が変わるかもしれません。
FROM debian:stable-slim as get WORKDIR /bun RUN apt-get update RUN apt-get install curl unzip -y RUN curl --fail --location --progress-bar --output "/bun/bun.zip" "https://github.com/oven-sh/bun/releases/download/bun-v1.0.1/bun-linux-x64-baseline.zip" RUN unzip -d /bun -q -o "/bun/bun.zip" RUN mv /bun/bun-linux-x64-baseline/bun /usr/local/bin/bun RUN chmod 777 /usr/local/bin/bun COPY package.json bun.lockb /bun COPY packages/app /bun/packages/app RUN bun install ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.11/litestream-v0.3.11-linux-amd64.tar.gz /tmp/litestream.tar.gz RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz FROM debian:stable-slim WORKDIR /work COPY --from=get /usr/local/bin/bun /bin/bun COPY --from=get /usr/local/bin/litestream /bin/litestream COPY --from=get /bun/node_modules /work/node_modules COPY --from=get /bun/packages /work/packages RUN apt-get update && \ apt-get install -y \ sqlite3 \ ca-certificates && \ rm -rf /var/lib/apt/lists/* RUN mv /work/packages/app/litestream.yml /etc RUN chmod +x /work/packages/app/entrypoint.sh WORKDIR /work/packages/app ENTRYPOINT ["./entrypoint.sh"]
SQLite3操作(bun:sqlite)
bun:sqlite
を使った読み込み/書き込み処理は以下の通りです。型の安全性はないですが、CURD処理を書くのに十分な機能がある印象でした。トランザクション処理もあります。詳しくはSQLite – API | Bun Docsが参考になります。型チェックをしっかり行いたい場合は、zod
を利用すると良いと思います。
import { Task } from "src/entity/task"; import { Database } from "bun:sqlite"; import { v4 as uuid } from "uuid"; import { DateTime } from "luxon"; const db = new Database("./todo.db"); const insertTask = db.prepare( "INSERT INTO task (task_id, title) VALUES ($taskId, $title);", ); const queryTask = db.query( "SELECT task_id, title, create_at FROM task WHERE task_id = $taskId;", ); const queryTaskAll = db.query("SELECT task_id, title, create_at FROM task;"); // 書き込み export const AddTask = async (title: string): Promise<Task> => { const taskId = uuid(); await insertTask.run({ $taskId: taskId, $title: title, }); const record = queryTask.get({ $taskId: taskId, }) as | { task_id: string; title: string; create_at: string; } | undefined; if (record == null) { throw new Error("unexpect insert exception"); } return { taskId: record.task_id, title: record.title, createAt: DateTime.fromSQL(record.create_at, { zone: "UTC" }), }; }; // 取り出し(単体) export const GetTask = async (taskId: string): Promise<Task | null> => { const record = queryTask.get({ $taskId: taskId, }) as | { task_id: string; title: string; create_at: string; } | undefined; if (record == null) { return null; } return { taskId: record.task_id, title: record.title, createAt: DateTime.fromSQL(record.create_at, { zone: "UTC" }), }; }; // 取り出し(複数) export const ListTask = async (): Promise<Task[]> => { const records = queryTaskAll.all() as { task_id: string; title: string; create_at: string; }[]; return records.map((record) => ({ taskId: record.task_id, title: record.title, createAt: DateTime.fromSQL(record.create_at, { zone: "UTC" }), })); };
最後に
本アプリ実装ではluxon
やuuid
といったモジュールを利用しましたが、問題なく動作しており、特にハマることなく既存のnpm資産を生かしつつアプリが作れました。またbun
bunx
がキビキビ動作して気持ちよく、今回のようなHTTP APIサーバーの実装はホットリロードと相性がよく開発体験がよかったです。ホットリロード自体は、Node.jsでも設定を入れたり、NestJSはCLI側で対応していたりはしますが、ネイティブサポートされていると小さく切り出してトラブルシュートなんかも手軽でいいですね。bun install
は高速でnpm互換があるため、CIのみbunを使う事例もあるそうです。今回試せませんでしたが、組み込みのテストツールも気になっています。
現時点で問題なく使えていますが、より他のランタイムと差別化要素や安定性が出てくると流行りそうな気がしています。
引き続きウォッチしていきたいと思います!